Understanding Properties, Property Observers, and Access Control in Swift
Course: iOS App Development
Topic: Understanding Properties, Property Observers, and Access Control
Course Link: iOS App Development Training at JustAcademy
Demo Link: Register for a Course Demo
Official Website: JustAcademy
1. Introduction
Properties, property observers, and access control are essential concepts in Swift programming. They help developers store and calculate values, respond to changes in data, and control how information can be accessed or modified within an application.
In iOS development, these concepts are commonly used in model classes, view controllers, SwiftUI applications, settings screens, user profiles, shopping carts, and data management systems.
For example, a shopping application might have a property called quantity. The app can use a stored property to maintain the quantity, a property observer to react when the quantity changes, and access control to prevent unrelated code from modifying internal data directly.
2. Learning Objectives
- Understand the meaning and purpose of properties in Swift.
- Differentiate stored, computed, lazy, and type properties.
- Learn the difference between mutable and constant properties.
- Implement getters and setters for computed properties.
- Use
willSet and didSet property observers.
- Understand Swift access levels:
open, public, internal, fileprivate, and private.
- Use
private(set) and related access modifiers to control write access.
- Apply properties and access control to practical iOS application models.
- Identify common mistakes and follow Swift coding best practices.
3. What Are Properties in Swift?
A property is a value associated with a class, structure, or enumeration. Properties provide a way to store information, calculate values, or manage access to an underlying value.
For example, a user profile may contain a name, email address, age, and account status. These values can be represented as properties inside a Swift type.
struct UserProfile {
var name: String
var email: String
var age: Int
}
var user = UserProfile(
name: "Aarav",
email: "[email protected]",
age: 25
)
print(user.name)
print(user.email)
print(user.age)
Output:
Aarav
[email protected]
25
In this example, name, email, and age are stored properties. Each instance of UserProfile maintains its own values.
4. Types of Properties in Swift
Swift supports several property categories. Understanding their differences helps you choose the right approach for a particular requirement.
| Property Type |
Purpose |
Example |
| Stored property |
Stores a value directly. |
var name = "Aarav" |
| Computed property |
Calculates a value using a getter and optionally a setter. |
var area: Double { width * height } |
| Lazy stored property |
Initializes a stored property when it is first accessed. |
lazy var data = loadData() |
| Type property |
Associates a value with the type itself rather than an individual instance. |
static var appName = "MyApp" |
| Property with observers |
Runs code before or after a property value is assigned. |
var score = 0 { didSet { ... } } |
5. Stored Properties
A stored property keeps a value as part of a class or structure instance. Stored properties can be declared with var when their values may change or let when their values should remain constant after initialization.
5.1 Variable Stored Property
struct Product {
var name: String
var price: Double
}
var product = Product(
name: "Wireless Mouse",
price: 799.0
)
product.price = 699.0
print(product.name)
print(product.price)
Output:
Wireless Mouse
699.0
The price property can be updated because it is declared with var and the instance itself is also declared with var.
5.2 Constant Stored Property
struct Course {
let courseCode: String
var courseName: String
}
var course = Course(
courseCode: "IOS101",
courseName: "iOS Development"
)
course.courseName = "Advanced iOS Development"
// course.courseCode = "IOS102"
// Error: courseCode is a constant property.
The courseCode property is a constant and cannot be reassigned after initialization. The courseName property remains mutable.
Important: When a structure instance is declared with let, its stored properties cannot be modified, even if those properties were declared using var.
6. Computed Properties
A computed property does not store a separate value. Instead, it calculates and returns a value whenever it is accessed. Computed properties can be defined in classes, structures, and enumerations.
6.1 Basic Computed Property
struct Rectangle {
var width: Double
var height: Double
var area: Double {
return width * height
}
}
var rectangle = Rectangle(
width: 10,
height: 5
)
print(rectangle.area)
rectangle.width = 12
print(rectangle.area)
Output:
50.0
60.0
The area property is calculated from width and height. When the width changes, the next access to area returns the newly calculated result.
6.2 Getters and Setters
A computed property can contain a getter to return a value and a setter to update other properties when a new value is assigned. A setter is optional, so a computed property can be read-only.
struct Temperature {
var celsius: Double
var fahrenheit: Double {
get {
return (celsius * 9 / 5) + 32
}
set(newFahrenheit) {
celsius = (newFahrenheit - 32) * 5 / 9
}
}
}
var temperature = Temperature(celsius: 25)
print(temperature.fahrenheit)
temperature.fahrenheit = 86
print(temperature.celsius)
Output:
77.0
30.0
The getter converts Celsius to Fahrenheit. The setter converts a supplied Fahrenheit value into Celsius and updates the stored property.
6.3 Read-Only Computed Properties
A read-only computed property has a getter but no setter. It can be written in a shorter form when its body contains a single expression.
struct Cart {
var itemPrices: [Double]
var totalPrice: Double {
itemPrices.reduce(0, +)
}
}
let cart = Cart(itemPrices: [199, 299, 499])
print(cart.totalPrice)
Output:
997.0
Because totalPrice is calculated from itemPrices, there is no need to store and manually synchronize a separate total.
7. Lazy Stored Properties
A lazy stored property is initialized when it is first accessed, rather than immediately when its instance is initialized. It is declared using the lazy keyword and must be a variable property.
class ReportManager {
lazy var reportData: [String] = loadReport()
func loadReport() -> [String] {
print("Loading report data...")
return ["Sales", "Customers", "Revenue"]
}
}
let manager = ReportManager()
print("Manager created.")
print(manager.reportData)
print(manager.reportData)
Output:
Manager created.
Loading report data...
["Sales", "Customers", "Revenue"]
["Sales", "Customers", "Revenue"]
The report data is loaded on the first access. Subsequent accesses use the initialized property instead of invoking the initialization expression again.
When to Use Lazy Properties
- When initialization requires expensive work.
- When a value may never be needed during the lifetime of an instance.
- When a property depends on other instance properties that are initialized first.
- When delaying resource creation can improve startup performance.
Lazy initialization should be designed carefully in concurrent code. If multiple threads access an uninitialized lazy property simultaneously, Swift does not guarantee that initialization happens only once.
8. Type Properties Using static and class
Instance properties belong to individual instances. Type properties belong to the type itself and are accessed through the type name.
8.1 Static Properties
struct AppConfiguration {
static let appName = "LearningApp"
static var supportedLanguage = "English"
}
print(AppConfiguration.appName)
print(AppConfiguration.supportedLanguage)
AppConfiguration.supportedLanguage = "Hindi"
print(AppConfiguration.supportedLanguage)
Output:
LearningApp
English
Hindi
All code accesses these values through AppConfiguration instead of through an instance.
8.2 Static and Class in Classes
In a class, static defines a type property that subclasses cannot override. A computed type property declared with class can be overridden by subclasses.
class ApplicationInfo {
static let platform = "iOS"
class var displayName: String {
return "Generic Application"
}
}
class LearningApplication: ApplicationInfo {
override class var displayName: String {
return "Learning Application"
}
}
print(ApplicationInfo.platform)
print(LearningApplication.displayName)
Output:
iOS
Learning Application
Stored type properties use static. The class modifier is used for overridable computed type properties in classes.
9. What Are Property Observers?
Property observers allow code to respond when a property's value is assigned. They are especially useful for tracking changes, validating values, logging updates, and triggering related actions.
Swift provides two property observers:
willSet: Executes immediately before the new value is stored.
didSet: Executes immediately after the new value is stored.
Observers can be used with stored properties you define and with eligible inherited properties that you override. Computed properties that you define yourself use their getter and setter to manage behavior rather than defining property observers directly.
10. Using willSet
The willSet observer runs before the property's value changes. It receives the incoming value, which is available as newValue unless you provide a custom parameter name.
struct UserAccount {
var username: String {
willSet(newUsername) {
print("Username will change to \(newUsername)")
}
}
}
var account = UserAccount(username: "aarav")
account.username = "aarav_dev"
Output:
Username will change to aarav_dev
The observer receives the proposed new value before it is assigned to username. A willSet observer is useful when you need to prepare for an update or record information about the incoming value.
11. Using didSet
The didSet observer executes after the property's new value has been stored. It provides the previous value through oldValue, unless you specify a custom parameter name.
struct ScoreBoard {
var score: Int = 0 {
didSet {
print("Previous score: \(oldValue)")
print("Current score: \(score)")
}
}
}
var board = ScoreBoard()
board.score = 10
board.score = 25
Output:
Previous score: 0
Current score: 10
Previous score: 10
Current score: 25
The observer is called each time the property is assigned, even when the new value equals the previous value. If you assign a different value to the same property inside its own didSet, that replacement does not trigger the observer recursively.
12. Using Both willSet and didSet
You can define both observers on the same property when you need to inspect the incoming value and then react after the assignment.
struct ProductInventory {
var stock: Int = 0 {
willSet {
print("Incoming stock: \(newValue)")
}
didSet {
print("Previous stock: \(oldValue)")
print("Updated stock: \(stock)")
}
}
}
var inventory = ProductInventory()
inventory.stock = 50
Output:
Incoming stock: 50
Previous stock: 0
Updated stock: 50
The output demonstrates the order of execution: willSet runs first, the value is updated, and then didSet runs.
13. Using Property Observers for Validation
Property observers can help maintain simple consistency rules. For example, an application might require a progress value to remain between zero and one hundred.
struct DownloadTask {
var progress: Int = 0 {
didSet {
if progress < 0 {
progress = 0
} else if progress > 100 {
progress = 100
}
}
}
}
var task = DownloadTask()
task.progress = 150
print(task.progress)
task.progress = -20
print(task.progress)
Output:
100
0
When a value outside the allowed range is assigned, the observer replaces it with the nearest valid boundary. The correction made inside didSet does not cause another recursive observer call.
Design note: For important domain rules, a computed property with a validating setter, a dedicated update method, or an initializer that enforces valid state may be clearer. Property observers are not a substitute for validation at every external input boundary.
14. Property Observers in Inheritance
A subclass can override an inherited property to add property observers. This allows it to respond when the inherited property changes without replacing the property's stored value with a new stored property.
class Employee {
var salary: Double = 30000
}
class Manager: Employee {
override var salary: Double {
didSet {
print("Salary updated from \(oldValue) to \(salary)")
}
}
}
let manager = Manager()
manager.salary = 45000
Output:
Salary updated from 30000.0 to 45000.0
This pattern can be useful when a subclass needs to track changes to an inherited property. The subclass must follow the rules for overriding properties, including matching the inherited property's type and respecting access control.
15. Introduction to Access Control
Access control determines which parts of a Swift application can access a type, property, method, initializer, or other declaration. It helps hide implementation details and define a safe, clear interface for other parts of an application.
For example, a banking application might allow other components to read an account balance while preventing them from changing the balance directly. The account type can expose a controlled method for transactions while keeping sensitive internal state private.
Swift provides five main access levels: open, public, internal, fileprivate, and private.
16. Access Control Levels in Swift
| Access Level |
Where It Can Be Accessed |
Common Use |
open |
Within the defining module and from other modules that import it; eligible classes and class members can be subclassed or overridden outside the defining module. |
Framework APIs designed for external subclassing. |
public |
Within the defining module and from other modules that import it. |
Public APIs and reusable framework types. |
internal |
Anywhere within the defining module. |
Default access level for most declarations. |
fileprivate |
Within the source file where the declaration is defined. |
Sharing implementation details among declarations in one file. |
private |
Within the enclosing declaration and its extensions in the same file, subject to Swift's private-access rules. |
Hiding implementation details inside a type. |
Important: public allows an API to be accessed from other modules but does not, by itself, permit external subclassing or overriding. open is the less restrictive level for eligible classes and class members intended for extension outside the module.
17. Using private Access Control
The private modifier restricts access to a declaration's permitted enclosing scope. It is useful for protecting implementation details that should not be accessed directly by unrelated code.
struct UserSettings {
private var password: String
init(password: String) {
self.password = password
}
func hasPassword() -> Bool {
return !password.isEmpty
}
}
let settings = UserSettings(password: "secure-value")
print(settings.hasPassword())
// This is not allowed outside the permitted scope:
// print(settings.password)
Output:
true
The password property is private, so external code cannot read it directly. The type provides a limited method that answers whether a password is present without exposing the stored password itself.
18. Using fileprivate Access Control
The fileprivate modifier allows access throughout the source file in which the declaration appears. It is useful when multiple related declarations in one file need to share an implementation detail.
fileprivate struct FormatHelper {
static func formatName(_ name: String) -> String {
return name.trimmingCharacters(
in: .whitespacesAndNewlines
)
}
}
struct ProfileViewModel {
func cleanName(_ name: String) -> String {
return FormatHelper.formatName(name)
}
}
let viewModel = ProfileViewModel()
print(viewModel.cleanName(" Aarav "))
Output:
Aarav
Both declarations can use FormatHelper because they are in the same source file. Code in a different source file cannot access this fileprivate type.
19. Using internal Access Control
internal is Swift's default access level. It allows declarations to be used throughout the same module, such as within the same application target or framework.
internal struct AppSettings {
var theme: String = "Light"
internal func displayTheme() {
print("Current theme: \(theme)")
}
}
let settings = AppSettings()
settings.displayTheme()
Output:
Current theme: Light
In a normal single-target iOS app, explicit internal modifiers are often unnecessary because Swift applies this access level by default.
20. Using public and open Access Control
When creating a reusable framework or module, you may need to expose selected types and members to other modules.
20.1 Public Example
public struct CourseInformation {
public let title: String
public init(title: String) {
self.title = title
}
public func displayTitle() -> String {
return title
}
}
The type, property, initializer, and method are explicitly public. A public type's members do not automatically become public; members intended for external use must have suitable access levels themselves.
20.2 Open Example
open class BaseScreenController {
public init() {}
open func configureScreen() {
print("Configuring base screen.")
}
}
An eligible open class can be subclassed outside its defining module, and an open method can be overridden there. Use open only when external customization is part of the framework's intended design.
21. Controlling Write Access with private(set)
Sometimes a property should be readable from outside a type but writable only inside that type. Swift supports this using access modifiers on a property's setter, such as private(set), fileprivate(set), internal(set), and package(set).
struct ShoppingCart {
private(set) var itemCount: Int = 0
mutating func addItem() {
itemCount += 1
}
mutating func removeItem() {
if itemCount > 0 {
itemCount -= 1
}
}
}
var cart = ShoppingCart()
cart.addItem()
cart.addItem()
print(cart.itemCount)
// This is not allowed outside the type:
// cart.itemCount = 100
cart.removeItem()
print(cart.itemCount)
Output:
2
1
The property can be read by external code, but its setter is private. Changes are made through the type's methods, which can enforce the required rules.
22. Combining Properties, Observers, and Access Control
These concepts are particularly useful when they work together. Consider a simple account model in which the balance can be read publicly but changes are performed only through controlled methods.
struct BankAccount {
private(set) var balance: Double = 0 {
didSet {
print("Balance changed from \(oldValue) to \(balance)")
}
}
mutating func deposit(_ amount: Double) {
guard amount > 0 else {
print("Deposit must be greater than zero.")
return
}
balance += amount
}
mutating func withdraw(_ amount: Double) {
guard amount > 0 else {
print("Withdrawal must be greater than zero.")
return
}
guard amount <= balance else {
print("Insufficient balance.")
return
}
balance -= amount
}
}
var account = BankAccount()
account.deposit(1000)
account.withdraw(250)
print("Final balance: \(account.balance)")
Output:
Balance changed from 0.0 to 1000.0
Balance changed from 1000.0 to 750.0
Final balance: 750.0
How the Example Works
balance is a stored property that maintains the account balance.
private(set) prevents external code from assigning arbitrary values directly.
didSet records changes to the balance.
deposit() accepts only positive deposits.
withdraw() checks the amount and prevents withdrawals exceeding the balance.
This is an educational example. Real financial systems require robust decimal arithmetic, transaction integrity, persistence, authorization, and additional security controls.
23. Applying Properties to SwiftUI
Properties are used extensively in SwiftUI, but SwiftUI's state-management wrappers have a different role from ordinary property observers. A regular stored property holds a value, while wrappers such as @State, @Binding, and @Published help manage data flow and view updates in their respective contexts.
23.1 Using a Computed Property in SwiftUI
import SwiftUI
struct PriceView: View {
let price: Double
let taxRate: Double
private var totalPrice: Double {
price + (price * taxRate)
}
var body: some View {
VStack(spacing: 12) {
Text("Price: \(price, specifier: "%.2f")")
Text("Tax: \(taxRate * 100, specifier: "%.0f")%")
Text("Total: \(totalPrice, specifier: "%.2f")")
.font(.headline)
}
.padding()
}
}
The computed property totalPrice calculates the amount displayed by the view. It avoids storing a second value that could become inconsistent with the price or tax rate.
23.2 Using @State for Editable Values
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.title)
Button("Increase") {
count += 1
}
Button("Reset") {
count = 0
}
}
.padding()
}
}
The @State property wrapper lets SwiftUI manage the counter's local state and update the view when the value changes. It is not the same as adding a didSet observer to an ordinary stored property.
24. Common Mistakes and Troubleshooting
| Problem |
Reason |
Solution |
| Cannot assign to a constant |
The property or containing value is immutable. |
Use var when mutation is required, or redesign the operation to return a new value. |
| Computed property cannot be assigned |
The computed property has no setter. |
Add a setter or treat the property as read-only. |
| Observer does not run during initialization |
Property observers do not run while a type initializes its own stored properties in its initializer body. |
Perform required setup explicitly in the initializer or another appropriate method. |
| Cannot access a private property |
The access is outside the permitted private scope. |
Expose a controlled method or property with the necessary access level. |
| Cannot set a private(set) property |
External code can read the value but cannot write it. |
Update it through an allowed method or change the setter access intentionally. |
| External framework subclassing fails |
The superclass or member is not open where external subclassing or overriding is required. |
Review the framework's intended API and use open where appropriate. |
| Incorrect computed result |
The calculation uses incorrect formulas or stale assumptions. |
Review the getter and verify its dependent values. |
| Repeated observer messages |
The property is assigned repeatedly, including when the same value is assigned. |
Compare old and new values if work is only required for actual changes. |
25. Best Practices
- Use stored properties for values that must be maintained over time.
- Use computed properties for values that can be derived from existing data.
- Use lazy properties when deferred initialization is beneficial and appropriate.
- Use
willSet to inspect incoming values and didSet to react after assignment.
- Avoid placing expensive work in observers that may execute frequently.
- Use access control to hide implementation details and expose only necessary functionality.
- Prefer
private(set) when other code needs to read a value but should not modify it directly.
- Keep public APIs small and stable when building reusable frameworks.
- Do not expose sensitive data merely to simplify access.
- Validate important external input explicitly, even when property observers are present.
- Use SwiftUI state-management tools according to their intended roles instead of relying on ordinary observers for all UI updates.
- Write unit tests for computed results, observer behavior, and access-controlled update methods.
26. Practical Exercises
Exercise 1: Student Profile
- Create a
Student structure with name, age, and course properties.
- Add a computed property that determines whether the student is an adult.
- Use
didSet on an eligible stored property to log changes.
- Make an internal identifier immutable after initialization.
Exercise 2: Product Inventory
- Create a product inventory model with a stock property.
- Use property observers to log stock changes.
- Prevent external code from changing stock directly.
- Implement methods to add stock and sell items.
- Prevent stock from becoming negative.
Exercise 3: App Settings
- Create an app settings type with a theme property.
- Add a computed property that displays a user-friendly theme description.
- Use a type property to store a default theme.
- Restrict internal settings using
private and expose suitable public-facing methods.
27. Mini Project: Shopping Cart Manager
Project goal: Build a small shopping cart model that combines stored properties, computed properties, property observers, and access control.
struct ShoppingCartManager {
private var prices: [Double] = []
private(set) var itemCount: Int = 0 {
didSet {
print("Item count: \(oldValue) -> \(itemCount)")
}
}
var totalPrice: Double {
prices.reduce(0, +)
}
mutating func addProduct(price: Double) {
guard price >= 0 else {
print("Price cannot be negative.")
return
}
prices.append(price)
itemCount = prices.count
}
mutating func clearCart() {
prices.removeAll()
itemCount = 0
}
}
var cart = ShoppingCartManager()
cart.addProduct(price: 499)
cart.addProduct(price: 799)
print("Items: \(cart.itemCount)")
print("Total: \(cart.totalPrice)")
cart.clearCart()
print("Final total: \(cart.totalPrice)")
Expected output:
Item count: 0 -> 1
Item count: 1 -> 2
Items: 2
Total: 1298.0
Item count: 2 -> 0
Final total: 0.0
Project Tasks
- Add a computed property that calculates a discount when the cart total exceeds a threshold.
- Add a type property for the default discount percentage.
- Prevent external code from modifying the internal prices array.
- Write unit tests for adding products, clearing the cart, and calculating totals.
- Extend the project with a SwiftUI screen that displays the item count and total.
28. Interview Questions and Answers
Q1. What is a property in Swift?
A property associates a value or computed behavior with a type. It can store a value, calculate a value, or control access to a value.
Q2. What is the difference between stored and computed properties?
A stored property maintains a value in storage. A computed property calculates its value through a getter and may provide a setter.
Q3. What is a lazy property?
A lazy stored property is initialized when it is first accessed rather than during the initial setup of the instance.
Q4. What is a property observer?
A property observer runs code before or after a property assignment. Swift provides willSet and didSet.
Q5. What is the difference between willSet and didSet?
willSet runs before the new value is stored and receives the incoming value. didSet runs after the value is stored and provides the previous value through oldValue.
Q6. Can a computed property have a property observer?
You cannot attach willSet or didSet directly to a computed property you define. Use its getter and setter for computed behavior. An eligible inherited computed property can be overridden with property observers.
Q7. What is access control?
Access control defines where a declaration can be accessed and helps protect implementation details.
Q8. What is the difference between private and fileprivate?
private restricts access to the permitted enclosing scope and same-file extensions. fileprivate permits access throughout the defining source file.
Q9. What is the difference between public and open?
Both can expose declarations outside the defining module. open additionally allows eligible classes to be subclassed and eligible class members to be overridden outside that module.
Q10. What does private(set) mean?
It allows a property to be read according to its getter access level while restricting writes to the property's private setter scope.
Q11. Can a property observer reject a value before it is assigned?
A property observer is not a dedicated validation gate. A setter, initializer, or update method is often better when an invalid value must be rejected before the stored value changes.
Q12. What is the difference between an instance property and a type property?
An instance property belongs to an individual instance. A type property belongs to the type itself and is accessed using the type name.
Q13. How do properties help in iOS development?
Properties store model data, derive values for presentation, manage configuration, and expose controlled interfaces for application state.
Q14. Are property observers the same as SwiftUI state management?
No. Property observers respond to assignments on eligible properties. SwiftUI property wrappers such as @State participate in SwiftUI's state and view-update system.
29. Quick Revision Checklist
- Stored properties maintain values.
- Computed properties calculate values using getters and optional setters.
- Lazy stored properties defer initialization until first access.
- Type properties belong to the type and commonly use
static.
willSet runs before a value is stored.
didSet runs after a value is stored and provides oldValue.
private and fileprivate restrict access to different scopes.
internal is the default access level for most declarations.
public exposes APIs across modules; open permits external subclassing and overriding where applicable.
private(set) supports public or otherwise permitted reading with restricted writing.
- Use methods or validating setters when invalid assignments must be rejected.
- SwiftUI state wrappers serve a different purpose from ordinary property observers.
30. Conclusion
Properties, property observers, and access control form an important foundation for building maintainable Swift and iOS applications. Stored and computed properties help manage data, lazy properties defer initialization, and type properties provide values associated with a type. Property observers allow code to respond to assignments, while access control protects internal implementation details and defines clear interfaces.
By combining these concepts, developers can create safer data models, reduce duplicated calculations, track important changes, and expose only the operations that other components need. Practice the exercises and mini project to build confidence using these techniques in real iOS development.
31. Additional Learning Resources